package cz.cuni.mff.ufal.volk.nlp;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.apache.log4j.Logger;

import jpl.Atom;
import jpl.Compound;
import jpl.JPLException;
import jpl.Query;
import jpl.Term;
import jpl.Util;
import jpl.Variable;
import cz.cuni.mff.ufal.volk.InitializationException;
import cz.cuni.mff.ufal.volk.Text;
import cz.cuni.mff.ufal.volk.UnsupportedLanguageException;
import cz.cuni.mff.ufal.volk.il.Node;
import cz.cuni.mff.ufal.volk.services.Understander;
import cz.cuni.mff.ufal.volk.utils.Utils;

/**
 * Provides a common wrapper for a Natural Language Understander written in SWI Prolog.
 *
 * <p>An underlying prolog program must provide the parse/2 predicate. parse(Sentence, T)
 * must succeed if the Sentence can be parsed into the Intermediate Language structure T.</p>
 *
 * @author Bartłomiej Etenkowski
 */
public class SwiplUnderstander implements Understander<Text> {

	private static final Logger log = Logger.getLogger(SwiplUnderstander.class);

	static {
		URL predefUrl = SwiplUnderstander.class.getResource("/nlu_prolog/pl/predef.pl");
		File predefTemp;
		try {
	    predefTemp = File.createTempFile("predef", ".pl");
	    predefTemp.deleteOnExit();
	    InputStream in = predefUrl.openStream();
	    if (in == null)
	    	throw new InitializationException("Could not open /nlu_prolog/pl/predef.pl resource file");
	    OutputStream out = new FileOutputStream(predefTemp);
	    try {
	    	long copied = Utils.copy(in, out);
	    	log.trace(String.format("%d bytes copied from %s to %s", copied,
	    			predefUrl.toString(), predefTemp.toString()));
	    } finally {
	    	out.close();
	    	in.close();
	    }
    } catch (IOException e) {
	    throw new InitializationException("An IOException was thrown during initialization of SwiplUnderstander class", e);
    }

    String command = String.format("consult('%s').", predefTemp.getAbsolutePath());
    command = command.replace('\\', '/'); // needed under Windows
		Query module = new Query(command);
		module.hasSolution();
		log.info("predefined predicates read");
	}

  /**
   * Constructs a SwiplUnderstander for the defined language. The grammar will be read
   * from the {@code plGrammar} file.
   *
   * @param  language underlying language
   * @param  plGrammar path to the file containing the Prolog grammar
   *
   * @throws NullPointerException if any of the parameters is {@code null}
   */
  public SwiplUnderstander(String language, String plGrammar) {
    language.getClass();
    plGrammar.getClass();
    this.language = language;
    this.plGrammar = plGrammar;
  }

  private final String moduleName = "module_" + UUID.randomUUID().toString().replace("-", "");
  private final String language;
  private final String plGrammar;

  private boolean swiplInitialized = false;
  private Query consultGrammar;

  private final static String TREE_NAME = "Tree";

  /**
   * If the swipl runtime has not been yet initialized the method consults the main grammar
   * file.
   */
  private void initializeSwiplIfNeeded() {
    if (!swiplInitialized) {
      log.info("consulting main grammar file...");
    	consultGrammar = new Query("consult(" + moduleName + ":'" + plGrammar + "')");
      consultGrammar.open();
      consultGrammar.getSolution();
      swiplInitialized = true;
      log.info("main grammar file consulted");
    }
  }

  /**
   * Tries to <i>understand</i> the natural language item given as parameter. It tries
   * to convert the expression into the Intermediate Language. The context can be used during
   * the understanding process, however, it should not be modified in the third layer.
   *
   * <p>It is possible that the understander will not be able to understand the message
   * completely (it is also possible that it will not understand it at all). In this case,
   * the result {@link Node} should contain a subnode marked with {@code understood=false}
   * attribute.</p>
   *
   * @param  item the message which has to be understood
   * @return tectogrammatical representation of the natural language expression
   *         in the intermediate language
   *
   * @throws NullPointerException if the item parameter is {@code null}
   * @throws UnsupportedLanguageException if the language of the message is not supported
   *         by the understander
   */
  @Override
  public Node understand(Text item) throws UnsupportedLanguageException {
    item.getClass();
    if (item.getLanguage() != null && !item.getLanguage().equals(language)) {
      throw new UnsupportedLanguageException(item.getLanguage());
    }

    try {
      initializeSwiplIfNeeded();
      String[] tokens = item.getText().split("[\\s]+");
      Term pl = createPrologList(tokens);
      Query qs = new Query("call_from_module", new Term[] { new Atom(moduleName), new Compound("parse", new Term[] { pl, new Variable(TREE_NAME) }) } );
      log.info("executing query call_from_module");
      qs.open();
      try {
        if (qs.hasMoreElements()) {
        	log.trace("qs.hasMoreElements = true");
        	Object result = qs.nextElement();
        	if (result != null && result instanceof Hashtable<?, ?>)
        		return getNode((Hashtable<?, ?>)result);
        	else {
        		log.warn("the query result cannot be cast to java.util.Hashtable, returning DNA");
        		return getDnaNode(item);
        	}
        } else {
        	log.trace("qs.hasMoreElements = false, returning DNA");
          return getDnaNode(item);
        }
      } finally {
        qs.close();
      }
    } catch  (JPLException e) {
      throw new RuntimeException("Internal prolog exception, see the cause for details", e);
    }
  }

  /**
   * Creates and returns a DNA (did-not-understand) node.
   */
  private Node getDnaNode(Text item) {
    Map<String, String> dna = new HashMap<String, String>(1);
    dna.put("dna", "true");
    return new Node(item.getText(), dna, null);
  }

  protected Node getNode(Hashtable<?, ?> solution) {
    Term cmp = (Term)solution.get(TREE_NAME);
    return getNode(cmp);
  }

  private Node getNode(Term cmp) {
    Map<String, List<Node>> children = getNodes(cmp.arg(3));
    String sem = cmp.arg(1).toString();
    Map<String, String> attr = getAttr(cmp.arg(2));
    return new Node(sem, attr, children);
  }

  private Map<String, String> getAttr(Term t) {
    Term[] terms = Util.listToTermArray(t);
    Map<String, String> res = new HashMap<String, String>(terms.length);
    for (Term term : terms) {
      res.put(term.arg(1).toString(), term.arg(2).toString());
    }
    return res;
  }

  private Map<String, List<Node>> getNodes(Term t) {
    Map<String, List<Node>> result = new HashMap<String, List<Node>>();
    Term[] terms = Util.listToTermArray(t);
    for (Term term : terms) {
      String sem = term.arg(1).toString();
      Term[] children = Util.listToTermArray(term.arg(2));
      List<Node> childNodes = new ArrayList<Node>(children.length);
      for (Term child : children) {
        childNodes.add(getNode(child));
      }
      if (result.containsKey(sem)) {
        List<Node> oldNodes = result.get(sem);
        for (Node n : childNodes) {
          oldNodes.add(n);
        }
      } else {
        result.put(sem, childNodes);
      }
    }
    return result;
  }

  private Term createPrologList(String[] tokens) {
    List<Term> terms = new ArrayList<Term>(tokens.length);
    for (String token : tokens) {
      if (!token.isEmpty()) {
        // TODO check commas etc.
        terms.add(new Atom(token.toLowerCase()));
      }
    }
    return Util.termArrayToList((Term[])terms.toArray(new Term[0]));
  }

}
